/**
 * 弹出层管理组件
 */
("use strict");
layui.define(["layer", "binder"], function (exports) {
  /***************************************************************************************************
   * 一.参数初始化
   * 可以在config参数中直接声明
   * 也可以使用外部引用的参数。
   *
   * WARNINGS: 参数必须包含一下几个部分
   *
   * constant     常量
   * base         响应式数据
   * temp         临时数据
   * pageDescribe 弹层参数配置
   * pageMultiple 复合临时参数缓存
   *                                                   *
   * HISTORY:                                            *
   *     @MethodAuthor: Malphite                                 *
   *     @Date: 2022-07-26         *
   *==================================================================================================*/

  // config变量，这里也可以直接将参数填入
  let config = {
    constant: {
      /**
       * 是否只展示最顶层的layer,
       * true：只展示最顶层的layer
       * false: 展示所有没有min的窗口
       */
      TOP_LAYER_ONLY: true,

      /**
       * 允许相同页面同时打开多个
       * true  允许
       * false 根据其它配置
       *
       *    说明:对于下面的参数 pageDescribe.porperty
       *        如果是配置singleton 的情况下，这样一个配置项只能保存一个窗口使用
       *        但是如果 DISPLAY_MULTIPLE_LAYER的值设置为true，那么它也可以同时被打开多个窗口实例
       */
      DISPLAY_MULTIPLE_LAYER: false,

      /**
       * 是否开启关闭特效
       * true  开启
       * false 使用layui默认特效
       *  一般设置为false, 试验的关闭特效，虽然某些情况下依然可以用
       */
      LAYER_CLOSE_EFFECTS: false,

      /**
       * 是否开启热更新快照
       * true  开启
       * false 关闭
       *    说明: 设计之初准备使用html2canvas实现html快照的功能，后来放弃了。
       *  现在已经不考虑这个功能了。和上一个配置LAYER_CLOSE_EFFECTS一样建议关闭
       */
      LAYER_SNAPSHOOT_UPDATE: false,

      /**
       * 是否启用worker提高页面反应
       * true  开启
       * false 关闭
       *   说明:这个配置是项目里面的其它功能使用的，这个选择关闭。
       * worker里面不能直接操作dom，所以在这里并不能提高什么。
       */
      ENABLE_WORKER: false,

      /**
       * 模拟选项卡，这里使用了layui admin里面的选项卡样式，最外层的id是照搬的
       */
      HEADERS_ID: "LAY_app_tabsheader",

      /**
       * 弹层最大层级zIndex的值
       */
      MAX_INDEX: 19891014,

      /**
       * 当前最高层级弹层的id
       * 这个id指的是layui.layer.open里面返回的id
       */
      TOP_INDEX: 0,

      /**
       * layui隐藏class
       */
      HIDE: "layui-hide",

      /**
       * 全局递增量
       */
      INTERNAL_INDEX: 0,

      /**
       * 消失动画中每个单元的宽度
       */
      RADIUS: 30,

      /**
       * 容器的id名称
       */
      CONTAINER_ID: "layui-windows-bottom",

      /**
       * 单例字符串
       */
      SINGLETON: "singleton",

      /**
       * layui模块自动加载后自动被执行的方法名称
       */
      METHOD_AUTOEXEC_NAME: "run",

      /**
       *
       * 这里面 需要给没有高层级的div标记上类选择器 MAX_CLASSNAME
       *
       * 这里面  MIN_CLASSNAME 需要配合css强行设置一个低的层级
       *
       * 在窗口最大化时，需要一些高层级的div 这个时候用类选择器 MAX_CLASSNAME来锁定
       * 将高层级的div添加一个class MIN_CLASSNAME 来降低他们的层级，可以保证他们不会遮挡窗口
       * 在窗口手动关闭或者最大化恢复时 去掉他们的类选择  MIN_CLASSNAME
       * 可以恢复他们原来的层级
       */
      MIN_CLASSNAME: "layui-min-header",
      MAX_CLASSNAME: "layui-max-header",

      /**
       * 预览窗口集合的外层className
       */
      CONTAINER_TIPS_CLASSNAME: "layui-windows-tip",

      /* 窗口弹出相关 */
      // 窗口初始向上方的偏移量
      BASE_WINDOWS_TOP_OFFSET: 70,
      // 窗口下方的偏移量， 下方的选项卡条也是要占地方的
      BASE_WINDOWS_BOTTOM_OFFSET: 85,
      // 窗口初始向左方的偏移量
      BASE_WINDOWS_LEFT_OFFSET: 10,
      // 窗口上下偏移方向 : 向下 DOWN  向上 UP
      BASE_WINDOWS_TOP_DIRECTION: "DOWN",
      // 窗口左右偏移方向 : 向左 LEFT  向右 RIGHT
      BASE_WINDOWS_LEFT_OFFSET: "RIGHT",
      // 每次窗口先旁边的偏移量
      BASE_WINDOWS_OFFSET: 30,
      // 默认的窗口宽度
      BASE_WINDOWS_WIDTH: 650,
    },
    base: {
      /**
       * 已经打开的窗口/分组
       *
       * children   子窗口列表(id)
       * select     这个组是否被选中
       *
       */
      livelyPool: [],

      /**
       * 鼠标移上去的小窗口的原始信息，方便移出之后的状态回滚
       * id 被预览的窗口id
       */
      hover: {},

      /**
       * 鼠标移上某个分组时，需要展示的小窗口信息
       */
      hoverPool: [],
    },
    temp: {
      /**
       * 活动中 窗口描述对象
       * id： 对应pageDescribe的key或者里面的id值
       * name： 窗口名称
       * select： true or false 是否被选中
       * updatetime: 最近一次活动(称为最顶层窗口)时的时间戳
       */
      windowsMap: {},
      resizeFn: {},
    },

    pageDescribe: {
      /**
       *
       *  key/id   这两个属性一样,最后open方法的第一个参数就是它
       * layui经典参数
       *
       * type     基本层类型  这里默认给 1
       * title    标题       这里默认没得，可以通过name属性自动生成
       * content  内容       默认''
       * skin                默认''
       * area     宽高       这里默认[650px, ***px]
       * offset   坐标       这里默认[70px, 10px]
       * ...
       * shade    遮罩       这里默认0
       * shift/anim 动画     这里默认1
       * fixed              这里默认false
       * maxmin   最大最小化  这里默认是true
       * ...
       *
       *
       * 自定义参数
       * name     窗口名称(如果没得title属性会 合并title <b> name </b>)
       * porperty 窗口实例策略 singleton(默认) 单例   property原型
       * snapshoot窗口快照地址,没有就填null
       * moudle   使用layui模块名称，如果有就会在页面成功打开之后自动载入并执行里面的run方法
       *
       *          *** 推荐使用这个方式生成的binder对象来进行视图控制
       *              内部集成的自动创建 调用run方式创建视图对象，然后
       *                            缓存到pageDescribe配置对象里面的VM下面
       *                            窗口关闭的时候会自动调用$destroy方法销毁VM
       *                       自动销毁 窗口关闭时自动调用destroy方法
       *                       自动resize 窗口resize时候自动调用视图对象里面 resize 方法重新渲染页面
       *
       * min      默认是false，窗口是否处于最小化状态
       *
       * 运行时产生的参数
       * topid: 父页面的id
       * porperty: 表明是child页面
       */
      bridge: {
        id: "bridge",
        name: "桥梁管理", // 合并title <b> name </b>
        porperty: "singleton",
        url: "",
        snapshoot: "../image/bridge.jpg",
      },
    },

    pageMultiple: {},
  };
  // 从外界config中传入参数
  // if (!config) config = CONFIG.windows;
  if (CONFIG) config = CONFIG.windows;
  // 快照参数处理,一般的图片最好处理一下，相对路径在css文件和js代码里面引用是可能发生路径问题
  // _.each(config.pageDescribe,(p) =>{
  //   if(_.isString(p.snapshoot)){
  //     var image=new Image();
  //     image.src= p.snapshoot;
  //     image.onload = function() {
  //       var canvas = document.createElement('canvas');
  //       var context = canvas.getContext('2d');
  //       canvas.width = image.width;
  //       canvas.height = image.height;
  //       context.drawImage(image, 0, 0);
  //       p.snapshoot = canvas.toDataURL("image/png",1);
  //     }
  //   }
  // });

  /***************************************************************************************************
   * 二.实现选项卡定位逻辑
   *
   * rollPage.left
   * rollPage.auto
   * rollPage.right
   *
   * 这里的逻辑是完全照搬layui admin里面的
   *                                                  *
   * HISTORY:                                            *
   *     @MethodAuthor: Malphite                                 *
   *     @Date: 2022-07-26         *
   *==================================================================================================*/

  let rollPage = {
    /**
     * 将内容向右边移动一个可视化距离
     * root.outerWidth()  可视化距离
     * prefLeft 下一步还能藏多远的距离，如果是正数说明不太够了，将第一项 left=0 的都要抽出来。
     */
    left: function (root, index) {
      // 1.首先获取到 菜单条  它距离容器左侧的距离
      let tabsLeft = parseFloat(root.css("left"));
      /**
       * 2.判断这个距离tabsLeft的值(这个值只能是小于等于00)
       *  情况一、这个值是等于0的，说明菜单条的左侧已经已经不能再向右边移动了。直接返回，不做改变
       * (仅仅使用  !tabsLeft  可能是 ''  或者 null  如果是 == 0 也不行 '' == 0 也是true
       *  所以满足 !tabsLeft 和  <= 0 两种条件的就只有 数字 0 了)
       *  情况二、这个值小于0
       */
      if (!tabsLeft && tabsLeft <= 0) return;
      /**
       * 3.计算需要移动的距离
       *  到此 tabsLeft必然小于0 ， root.outerWidth()菜单可视宽度是大于0 的
       *  -(tabsLeft + root.outerWidth())    ==>  - -tabsLeft  - root.outerWidth();
       *  - -tabsLeft 是菜单条超过左侧的距离
       *  那么prefLeft的实际意义是  菜单条 向右移动一个 菜单可视宽度，此时  菜单条和容器左侧的距离
       *
       *
       *
       *  prefLeft：首先使用菜单可视宽度(root.outerWidth())加上tabsLeft,得到移动后，原来展示的信息可保留的最大距离
       *    ( 相当于可视距离减去移动被替换的距离，得到剩下可保留的原来的最大距离 )
       *    因为这个tabsLeft必然小于0，所以最后的结果必然小于 root.outerWidth()
       *    情况一、如果这个距离大于0等于0，你左边超出的部分，菜单可视宽度完全可以展示出来，说明只需要把左边超出的部分移动展示出来。
       *    情况二、如果这个距离小于0，说明你左边超出的部分，要想一次展示出来，整个菜单可视距离都利用上还不够，只能展示一部分。
       */
      let prefLeft = -(tabsLeft + root.outerWidth());
      if (prefLeft >= 0) return root.css("left", 0);
      /**
       * 现在假设 强行将菜单的left设置为了0，菜单的左侧就对齐了，那么右侧会超出来一大截，超出的距离就是 prefLeft的等值
       * 此时
       * 依次遍历所有的li标签  它们left值第一个是0 后面慢慢增大
       * 当left值增加到等于或者超过 ‘prefLeft的等值’ 时，此时如果这个点处在菜单可视化左侧的0点，可以认为这样就刚刚好向右移了一个可视化距离
       *       a                b
       * |__________________|_________|   如果想求a比比长多少，可以将两个线段重合起来比较
       *               a-b
       * |___________|______|
       */
      root.children("li").each(function (index, item) {
        let li = $(item),
          left = li.position().left;
        if (left >= prefLeft) {
          root.css("left", -left);
          return false;
        }
      });
    },
    /**
     * 将所选中的内容展示到菜单可视范围内
     */
    auto: function (root, index) {
      let tabsLeft = parseFloat(root.css("left"));
      // 获得被选中li标签
      let thisLi = root.find('[lay-id="' + index + '"]');
      if (!thisLi[0]) return;
      let thisLeft = thisLi.position().left;
      // tabsLeft 必然是一个负数  -tabsLeft 指的是root藏住的长度
      // 如果 thisLeft < -tabsLeft 代表这个li被藏在左边了
      // 那就直接把它放在左边第一个的位置
      if (thisLeft < -tabsLeft) {
        return root.css("left", -thisLeft);
      }
      // thisLeft + thisLi.outerWidth() 指的是li标签的尾部到root头部的距离
      // outerWidth - tabsLeft 指的是可视的尾部到root头部的距离
      // li被藏在了右边看不全
      if (thisLeft + thisLi.outerWidth() >= root.outerWidth() - tabsLeft) {
        // 计算被藏住的长度
        let subLeft =
          thisLeft + thisLi.outerWidth() - (root.outerWidth() - tabsLeft);
        root.children("li").each(function (i, item) {
          let li = $(item),
            left = li.position().left;
          if (left + tabsLeft > subLeft) {
            root.css("left", -left);
            return false;
          }
        });
      }
    },
    /**
     * 将内容向左边移动一个可视化距离
     */
    right: function (root, index) {
      let tabsLeft = parseFloat(root.css("left"));
      // left + li.outerWidth() li标签的位置
      // root.outerWidth() - tabsLeft 被展示到的最远位置
      // 将第一个在右边被遮住的li放在第一个展示
      root.children("li").each(function (index, item) {
        let li = $(item),
          left = li.position().left;
        if (left + li.outerWidth() >= root.outerWidth() - tabsLeft) {
          root.css("left", -left);
          return false;
        }
      });
    },
  };

  // 定义整个菜单控制模块
  layui.binder.extend({
    name: "windows",
    data: config.base,
    template: `
      <div id="${config.constant.CONTAINER_ID}" class = "${config.constant.MAX_CLASSNAME}">
        <div class="layui-windows-menu" title = "打开菜单" @click = "showMenu">
          <i class="layui-icon ">&#xe609;</i>
        </div>
        <div class="layui-windows-group">
          <div class="layui-icon layadmin-tabs-control layui-icon-prev" @click="rollPageLeft"></div>
          <div class="layui-icon layadmin-tabs-control layui-icon-next" @click="rollPageRight"></div>
          <div class="layui-tab">
            <ul class="layui-tab-title" id="${config.constant.HEADERS_ID}" v-each="livelyPool" mark="arr" key = "index">
              <li :class="layui-tab-title-{{{{arr.select}}}}" lay-id="#{arr.id}"  @mouseenter="showWindowTip"  @mouseleave="hideWindowTip"  @click="setGroupTop(#{arr.id})" >
                <i class="layui-icon">#{arr.icon == undefined ? arr.name : arr.icon}</i>
              </li>
            </ul>
          </div>
        </div>
        <div class="layui-windows-notice" title = "关闭全部" @click = "closeAll" @mouseenter="appendHover(0)" @mouseleave="removeHover(0)" @mousemove = "moveHover(0)">
          <i class="layui-icon layui-icon-close"></i>
        </div>
        <div class="${config.constant.CONTAINER_TIPS_CLASSNAME}"  @mouseenter="hoverTip"  @mouseleave="hideWindowTip" v-each="hoverPool" mark="arr" key = "id"  >
          <div class = "layui-windows-tips"  @mouseenter="appendHover(#{arr.id})" @mouseleave="removeHover(#{arr.id})" @mousemove = "moveHover(#{arr.id})"  wid = "#{arr.id}" @click="setHoverTop(#{arr.id})">
            <i class="layui-icon layui-icon-close" @click="closeFromTip" title = "关闭" ></i>
            <div>#{arr.name}</div>
          </div>  
        </div>
      </div>
    `,
    watchs: {
      hover(value) {
        if (value.id) {
          this.tabsBody(value.id)
            .removeClass("layui-windows-preview")
            .addClass("layui-windows-preview");
          $("body")
            .removeClass("layui-windows-preview")
            .addClass("layui-windows-preview");
        } else if (value.id == 0) {
          $("body")
            .removeClass("layui-windows-preview")
            .addClass("layui-windows-preview");
        } else {
          $(".layui-windows-preview").removeClass("layui-windows-preview");
        }
      },
    },
    methods: {
      rollPageLeft() {
        rollPage.left(this.parent.find("#" + config.constant.HEADERS_ID));
      },
      rollPageRight() {
        rollPage.right(this.parent.find("#" + config.constant.HEADERS_ID));
      },
      rollPageAuto(index) {
        rollPage.auto(
          this.parent.find("#" + config.constant.HEADERS_ID),
          index
        );
      },
      /**
       * 获取指定的窗口的jq对象
       * @param {*} index 这个是配置里面的key 或者里面的id属性
       */
      tabsBody(index) {
        return config.pageDescribe[index].parent;
      },
      /**
       * 将指定的窗口置顶
       * @param {*} index 这个指在pageDescribe配置项里面的key或者id，可以通过open方法弹出该窗口
       */
      setTop(index = config.constant.TOP_INDEX) {
        let that = this;
        if (!index) return;
        // 获取配置最高层级
        let zIndex = _.cloneDeep(config.constant.MAX_INDEX);
        if (config.constant.TOP_INDEX && config.constant.TOP_LAYER_ONLY) {
          // 如果此时有最顶层窗口id，并且开启只展示最顶层的窗口
          // 就要先将原来的最顶层的窗口隐藏
          that
            .tabsBody(config.constant.TOP_INDEX)
            .removeClass(config.constant.HIDE)
            .addClass(config.constant.HIDE);
          config.pageDescribe[config.constant.TOP_INDEX].show = false;
        }
        /**
         * 更新zIndex值，达到结果最近访问的窗口排在上面(预留最顶层的位置)
         * 1.根据当前更新时间，由近时间到远时间对 windowsMap 进行排序
         * 2.在1的顺序下遍历它，并更新每个窗口的zIndex的值
         */
        _.each(
          _.reverse(_.sortBy(config.temp.windowsMap, (u) => u.updateTime)),
          function (v) {
            that.tabsBody(v.id).css("z-index", --zIndex);
            // 记录当前的层级，在windows预览恢复的时候恢复为该层级
            config.pageDescribe[v.id].nowIndex = zIndex;
          }
        );

        /**
         * 遍历 活动组池里面的信息，1.更新当前选中的组 2.更新最顶层的配置更新时间。
         *
         * 1.遍历整个组。
         * 2.在每个组的分组子窗口中检索，如果和传入的index匹配，就修改它的窗口描述对象，将更新时间更新到现在。
         * 3.如果在这个组的子窗口匹配上传入的index则将这个分组的select值修改为true，反之为false
         */
        _.each(that.livelyPool, function (v) {
          let childernflag = false;
          _.each(v.children, function (v1) {
            if (v1 == index) {
              config.temp.windowsMap[v1].updateTime = new Date();
              childernflag = true;
            }
          });
          v.select = childernflag;
        });

        /**
         * 修改index对应的窗口
         * 1.显示这个窗口
         * 2.zIndex修改为最大的index
         */
        that
          .tabsBody(index)
          .css("z-index", config.constant.MAX_INDEX)
          .removeClass(config.constant.HIDE);
        config.pageDescribe[index].nowIndex = config.constant.MAX_INDEX;
        config.pageDescribe[index].show = true;
        // 最小化的恢复
        if (!!config.pageDescribe[index].min) {
          that.restore(index);
          config.pageDescribe[index].min = false;
        }
        // 更新topIndex
        config.constant.TOP_INDEX = index;
        // 菜单选项卡自适应
        that.rollPageAuto(index);
      },

      /**
       * 添加resize 事件
       * @param {*} id
       * @param {*} fn
       */
      resize: function (id, fn) {
        // 如果之前已经绑定过了，就进行解绑操作
        if (config.temp.resizeFn[id]) {
          $(window).off("resize", config.temp.resizeFn[id]);
          delete config.temp.resizeFn[id];
        }
        //如果是清除 resize 事件，则终止往下执行
        if (fn === "off") return;
        // 添加resize事件
        config.temp.resizeFn[id] = function () {
          debounce(fn, { args: [id] });
        };
        $(window).on("resize", config.temp.resizeFn[id]);
      },

      /**
       *
       * 打开一个图层(核心方法)
       * @param {*} index pageDescribe配置项里面的key或者里面的id
       * @param {Object/Function} data 传入的参数，或者一个回调函数，回调函数的参数config是对应的pageDescribe配置项
       * @returns  无，是用第二个参数是回调参数的方式
       *
       * 1.判断已有窗口是否可以复用
       * 2.处理pageDescribe窗口配置对象
       * 3.组装layui.layer.open 参数配置对象option
       * 4.通过配置对象调用方法弹出窗口
       */
      open(index, data = {}) {
        // 已打开的页面无需重新打开
        if (config.pageDescribe[index].index) {
          // 将选择的窗口置顶
          this.setTop(index);
          // 如果第二个参数是回调函数就执行这个回调函数
          _.isFunction(data) && data(config.pageDescribe[index]);
          return;
        }
        // 计算页面位置(这个作为参考，最后还是以配置项参数为准)
        let position = this.position(data.area);
        // 获取对应的配置项参数
        let _config = config.pageDescribe[index];
        /**
         * 设置非单例模式，或者允许多次打开
         * 特殊处理：生成动态配置对象
         */
        if (
          _config.porperty != config.constant.SINGLETON ||
          config.constant.DISPLAY_MULTIPLE_LAYER
        ) {
          // 多窗口去重标志
          // 多个窗口也可以通过传入的id or code等附加参数，锁定重用
          let key = data.id || data.code || 0;
          // 查看多个弹框的缓存
          if (config.pageMultiple[index]) {
            // 如果存在相同 附加参数 的窗口 就直接复用之前的窗口
            if (key && config.pageMultiple[index][key]) {
              this.setTop(config.pageMultiple[index][key]);
              // 如果第二个参数是回调函数就执行这个回调函数
              _.isFunction(data) && data(config.pageDescribe[index]);
              return;
            }
          }
          // 没有缓存就需要创建
          if (!config.pageMultiple[index]) config.pageMultiple[index] = {};
          // 生成临时key or id
          let _index = "TEMP_" + config.constant.INTERNAL_INDEX;
          // 如果有锁定信息就将锁定信息也加入
          if (key) {
            config.pageMultiple[index][key] = _index;
          }
          // 全局变量自增
          config.constant.INTERNAL_INDEX++;
          /**
           * 组装临时pageDescribe配置对象
           * 1.name属性修改，根据追加参数的name属性进行改变
           * 2.将传入的index记录为topid
           * 3.申明这个临时配置对象是一个子对象
           * 4.继承父对象的快照属性
           */
          config.pageDescribe[_index] = _.assign({}, _config, {
            id: _index,
            name:
              data && data.name ? _config.name + "-" + data.name : _config.name,
            topid: index,
            porperty: "child",
            snapshoot: _config.snapshoot,
          });

          /**
           * 将这个外面确定的 _config 配置对象和 index 替换为新生成的
           */
          _config = config.pageDescribe[_index];
          index = _index;
        }
        // title属性特殊处理，用name属性来生成 
        if (!_config.title) _config.title = `${_config.icon == undefined ? '' : `<i class="layui-icon">${_config.icon}</i>&nbsp;&nbsp;&nbsp;`}<b>${_config.name}</b>`;
        // 合并参数，构建layui.layer.open 所需要的参数
        let option = _.assign(
          {
            type: 1,
            shade: 0,
            shift: 1,
            fixed: false,
            maxmin: true,
            offset: [position.top + "px", position.left + "px"],
            area: [position.width + "px", position.height + "px"],
          },
          _config
        );
        let that = this;
        /**
         * 窗口成功打开之后的回调函数
         * @param {*} layero   如果是弹出窗口层，就是窗口的jq对象
         * @param {*} layerid  layui.layer.open的返回结果，弹层的id
         */
        option.success = function (layero, layerid) {
          // 执行windows组件里面的默认回调方法
          that.success(layero, layerid, index);
          /**
           * 假如配置项里面填入了moudle属性,就使用layui方法引用这个模块，并自动执行里面的run方法
           * 1.判断配置项是否存在，并且这个配置项是一个字符串
           * 2.使用layui的use方法载入这个模块
           * 3.判断模块里面是否有紫执行方法，并执行它
           * 4.按照需要，给这个窗口添加resize事件
           * 5.执行回调函数
           */
          if (_config.moudle && _.isString(_config.moudle)) {
            // 载入模块
            layui.use(_config.moudle, function () {
              // 执行自执行方法
              /**
               * 参数:
               * layero: 如果是弹出窗口层，就是窗口的jq对象
               * layerid: layui.layer.open的返回结果，弹层的id
               * index: 这个指在pageDescribe配置项里面的key或者id，可以通过open方法弹出该窗口
               * data: 第二个参数，如果是一个Object对象，可以以这个方式传入窗口中
               * _config: 这个窗口对应的pageDescribe配置项
               *  ****建议窗口里面使用binder创建自己的视图对象，并保存到自己的配置对象的VM属性中，方便后面集中管理
               */
              layui[_config.moudle] &&
                layui[_config.moudle][config.constant.METHOD_AUTOEXEC_NAME] &&
                layui[_config.moudle][config.constant.METHOD_AUTOEXEC_NAME](
                  layero,
                  layerid,
                  index,
                  data,
                  _config
                );
              // 添加resize事件
              layui[_config.moudle] &&
                layui[_config.moudle].resize &&
                that.resize(index, layui[_config.moudle].resize);
              // 执行回调
              // 如果第二个参数是回调函数就执行这个回调函数
              _.isFunction(data) && data(_config);
            });
          } else {
            // 执行回调
            // 如果第二个参数是回调函数就执行这个回调函数
            _.isFunction(data) && data(_config);
          }
          // 执行传入的配置项参数里面用户自定义的成功回调函数
          _config.success && _config.success(layero, layerid, index, data);
        };

        /**
         * 窗口关闭之后的回调函数
         *
         */
        option.end = function () {
          // 执行windows组件默认的关闭回调函数
          that.end(index);
          /**
           * 如果配置了moudle配置项，那么在成功回调里面就已经执行过自执行函数了。
           * 1. 那么这里需要执行配套的执行销毁方法
           * 2. 根据上面的建议，需要释放视图对象
           */
          if (_config.moudle) {
            // 执行配套的销毁方法
            layui[_config.moudle] &&
              layui[_config.moudle].destroy &&
              layui[_config.moudle].destroy(index, _config);
            // 释放视图对象
            // 上面的销毁方法里面就应该释放视图对象，这个是清除这个引用
            if (_config.VM) {
              _config.VM.$destroy();
              _config.VM = null;
            }
            // 移除resize事件
            layui[_config.moudle] &&
              layui[_config.moudle].resize &&
              that.resize(index, "off");
          }
          // 执行传入的配置项参数里面用户自定义的失败回调函数
          _config.end && _config.end(index);
        };

        /**
         * 用户点击关闭按钮后触发的事件
         * @param {*} layerid layui.layer.open的返回结果，弹层的id
         * @param {*} layero  如果是弹出窗口层，就是窗口的jq对象
         */
        option.cancel = function (layerid, layero) {
          // 首先如果窗口之前最大化过，需要恢复哪些高层级div
          // 其实就是给 max 类标记的div 取消掉  min 的 class
          // ** 因为max类是手动标记的，不能确定全是这个组件的parent里面的，所以直接用jq选择器去选择
          $("." + config.constant.MAX_CLASSNAME).removeClass(
            config.constant.MIN_CLASSNAME
          );

          //执行配置项里面用户自定义的cancel方法
          if (_config.cancel && _.isFunction(_config.cancel)) {
            _config.cancel(layerid, layero, function () {
              // 即使是用户自定义的方法，也要执行windows组件的beforeDestroy销毁窗口配置
              that.beforeDestroy(index);
            });
            return false;
          }
          // 执行windows组件的beforeDestroy销毁窗口配置
          that.beforeDestroy(index);
          return false;
        };

        /**
         * 用户点击最大化按钮后触发的事件
         */
        option.full = function () {
          // 其实就是给 max 类标记的div 新增一个 min 的 class
          $("." + config.constant.MAX_CLASSNAME)
            .removeClass(config.constant.MIN_CLASSNAME)
            .addClass(config.constant.MIN_CLASSNAME);
          // 执行窗口resize事件
          !!config.temp.resizeFn[index] && config.temp.resizeFn[index]();
        };

        /**
         * 用户点击恢复窗口按钮后触发的事件
         */
        option.restore = function () {
          // 其实就是给 max 类标记的div 取消掉  min 的 class
          $("." + config.constant.MAX_CLASSNAME).removeClass(
            config.constant.MIN_CLASSNAME
          );
          // 执行窗口resize事件
          !!config.temp.resizeFn[index] && config.temp.resizeFn[index]();
        };

        /**
         * layui.layer原生窗口resize事件回调函数
         */
        option.resizing = function () {
          // 执行窗口resize事件
          !!config.temp.resizeFn[index] && config.temp.resizeFn[index]();
        };

        /**
         * 接下来是确定content配置型，然后调用 layui.layer.open方法弹出窗口了
         * 1.配置项里面包含url参数。使用jquery的ajax  get到对应的值，将这个值赋给content然后弹出窗口
         *    为了提高这个方法的适用性，这里的url获取到的内容并没有任何处理。
         *    建议:  窗口里面的带权限、需要过滤、敏感信息 等可以自己过滤后动态的放入dom中
         *    比如:  url -> 一个窗口的外壳
         *           moudle -> 加载一个组件
         *           ** 组件里面的template 通过 layui.authHandler.pageAuthFilter 鉴权方法进行过滤
         *           最后展示的页面就是过滤后的页面了
         *
         * 2.如果没有配置url，也没有配置content配置项。就将name组成content配置项
         * 3.没有url配置项，但是有content配置项就直接调用方法弹出窗口
         */
        if (_config.url) {
          $.get(_config.url, {}, function (res) {
            option.content = res;
            layui.layer.open(option);
          });
        } else {
          if (!option.content)
            option.content = "<div>" + option.name + "</div>";
          layui.layer.open(option);
        }
      },

      /**
       * 计算页面位置和大小
       * 根据传入的宽度(默认650) 计算窗口的高度
       * 根据浏览器窗口的高宽确定使用  高度/宽度 or 宽度/高度  = 0.618
       * @param {*} area  窗口宽度
       * @returns
       */
      position(area) {
        /**
         * 1. 获取窗口的总高度和总宽度
         */
        let $body = $("body");
        let rootWidth = $body.width();
        let rootHeight = $body.height();
        /**
         * 2. 获取窗口宽度
         * 如果传进来的是宽度(一个字符串)就以它为准
         * 否则使用默认宽度 config.constant.BASE_WINDOWS_WIDTH
         */
        let width =
          !!area && _.isString(area)
            ? parseInt(area)
            : config.constant.BASE_WINDOWS_WIDTH;
        // 高宽比例
        let [k, k1] = [0.618, 1.618];
        // 3. 判断当前是否选中了一个窗口  true  没有  false 有
        let flag = !config.constant.TOP_INDEX;
        // 4. 偏移的步数，如果没有就不偏移
        let step = flag ? 0 : 1;
        // 5. 当前距离上方的偏移量，有选中窗口去选中窗口当前的值，否则取默认值
        let nowTop = flag
          ? config.constant.BASE_WINDOWS_TOP_OFFSET
          : parseInt(this.tabsBody(config.constant.TOP_INDEX).css("top"));
        // 6.当前距离左边的偏移量，有选中窗口去选中窗口当前的值，否则取默认值
        let nowLeft = flag
          ? config.constant.BASE_WINDOWS_LEFT_OFFSET
          : parseInt(this.tabsBody(config.constant.TOP_INDEX).css("left"));
        // 7.获取接下来的偏移量,根据当前的top和left与现在的偏移方向来计算出接下来的窗口它预计的top和left偏移量
        let top =
          "DOWN" == config.constant.BASE_WINDOWS_TOP_DIRECTION
            ? nowTop + config.constant.BASE_WINDOWS_OFFSET * step
            : nowTop - config.constant.BASE_WINDOWS_OFFSET * step;
        let left =
          "RIGHT" == config.constant.BASE_WINDOWS_LEFT_OFFSET
            ? nowLeft + config.constant.BASE_WINDOWS_OFFSET * step
            : nowLeft - config.constant.BASE_WINDOWS_OFFSET * step;
        // 8.计算窗口高度, 总高度 - （最大高度 + 上下偏移量） 判断以最大的高度能不能容得下
        let testHeight =
          rootHeight -
          width * k1 -
          top -
          config.constant.BASE_WINDOWS_BOTTOM_OFFSET;
        // 最大高度都够的话就取最大高度，否则取最小高度
        let height = testHeight >= 0 ? width * k1 : width * k;

        //9.接下来是一系列的微调操作
        /**
         * 计算窗口会不会超出右边
         * 总宽度 - left偏移量  - 宽度  看看还有没有余下来的宽度
         * 如果超出就先对齐右边: left偏移量取 总宽度 - 宽度
         *        然后将接下来的偏移方向修改为  LEFT  向左偏移
         * 如果没有超出就暂时不管
         */
        if (rootWidth - left - width < 0) {
          // 靠右
          left = rootWidth - width;
          // 更新方向向左了
          config.constant.BASE_WINDOWS_LEFT_OFFSET = "LEFT";
        }
        /**
         * 计算窗口会不会超出左边最小偏移区域
         * 看看left偏移量有没有小于最小偏移量 config.constant.BASE_WINDOWS_LEFT_OFFSET
         * 如果超出就先对齐左边: left偏移量取 最小偏移量 config.constant.BASE_WINDOWS_LEFT_OFFSET
         *        然后将接下来的偏移方向修改为  RIGHT  向右偏移
         * 如果没有超出就暂时不管
         */
        if (left < config.constant.BASE_WINDOWS_LEFT_OFFSET) {
          // 靠左
          left = config.constant.BASE_WINDOWS_LEFT_OFFSET;
          // 更新方向向右了
          config.constant.BASE_WINDOWS_LEFT_OFFSET = "RIGHT";
        }

        /**
         * 计算窗口会不会超出下方
         * 总高度 - top偏移量  - 高度  看看还有超过下方最少剩余的位置(下方至少要留一个选项卡条的高度)
         * 如果超出就先对齐下方: top偏移量取 总高度 - 高度 - 下方最少剩余的位置config.constant.BASE_WINDOWS_BOTTOM_OFFSET
         *        然后将接下来的偏移方向修改为  UP  向上偏移
         * 如果没有超出就暂时不管
         */
        if (
          rootHeight - top - height <
          config.constant.BASE_WINDOWS_BOTTOM_OFFSET
        ) {
          // 靠下
          top =
            rootHeight - height - config.constant.BASE_WINDOWS_BOTTOM_OFFSET;
          // 更新方向向上了
          config.constant.BASE_WINDOWS_TOP_DIRECTION = "UP";
        }
        /**
         * 计算窗口会不会超出上方
         * 看看top偏移量有没有小于最小偏移量 config.constant.BASE_WINDOWS_TOP_OFFSET
         * 如果超出就先对齐上方: top偏移量取 最小偏移量 config.constant.BASE_WINDOWS_TOP_OFFSET
         *        然后将接下来的偏移方向修改为  DOWN  向下偏移
         * 如果没有超出就暂时不管
         */
        if (top < config.constant.BASE_WINDOWS_TOP_OFFSET) {
          // 靠上
          top = config.constant.BASE_WINDOWS_TOP_OFFSET;
          // 更新方向向下了
          config.constant.BASE_WINDOWS_TOP_DIRECTION = "DOWN";
        }

        //10.返回测算结果
        return {
          top: top,
          left: left,
          width: width,
          height: height,
        };
      },

      /**
       * 成功打开页面之后的回调函数
       * @param {*} layero  如果是弹出窗口层，就是窗口的jq对象
       * @param {*} index   layui.layer.open的返回结果，弹层的id
       * @param {*} id      这个指在pageDescribe配置项里面的key或者id，可以通过open方法弹出该窗口
       */
      success(layero, index, id) {
        // 在窗口配置对象中写入layui返回结果，表明这个窗口已经被打开，可以使用
        config.pageDescribe[id].index = index;
        // 在窗口配置对象中写入layui窗口的jq对象方便后面的调用
        // ** 弹出窗口层这里就可以了，弹出的是iframe层就需要自己调方法进行处理了
        config.pageDescribe[id].parent = layero;
        // 设置最小化标志为false
        config.pageDescribe[id].min = false;
        // 设置窗口可视化标志为true
        config.pageDescribe[id].show = true;
        // 下面进行窗口组管理
        // 获取父ID
        let parentid = config.pageDescribe[id].topid || id;
        // 获取父名称
        let parentname = config.pageDescribe[parentid].name;
        //加入windows描述中
        config.temp.windowsMap[id] = {
          id: id,
          name: config.pageDescribe[id].name,
          select: false,
          updateTime: new Date(),
        };
        // 检查父id是否存在
        // 可能这个分组下面的其它窗口神明时已经添加了父id的声明
        let parentflag = false;
        _.each(config.base.livelyPool, function (v) {
          if (v.id == parentid) {
            // 如果找到对应的父id就将这个标记置为true
            parentflag = true;
            let childflag = false;
            // 遍历父id对应的children ，只有children里面没有当前的id才在里面加入
            _.each(v.children, function (v1) {
              if (v1 == id) childflag = true;
            });
            if (!childflag) v.children.push(id);
          }
        });
        // 如果没有找到父id对应的内容，这里就需要立即创建
        if (!parentflag) {
          // 向list里面加入
          // **这里之所以不直接修改list是因为list这个数据类型没有特殊处理过，暂不支持响应式插入
          // 上面的children是放弃了响应式的,所以是直接push的
          // index 取递增量主要是作为渲染时遍历的主键，防止dom的重复渲染
          let list = _.cloneDeep(config.base.livelyPool);
          list.push({
            index: ++config.constant.INTERNAL_INDEX,
            id: parentid,
            name: parentname,
            select: false,
            icon: config.pageDescribe[id].icon,
            children: [id],
          });
          config.base.livelyPool = list;
        }

        /**
         * 最小化事件修改
         *
         * layui.layer里面的原生最小化事件是将窗口缩小成一块，摆放到下方。
         * windows组件就是管理这些窗口的，在最小化时是不展示窗口的状态，需要点击对应的选项卡才会显示。这个事件必须更改
         */
        // 最小化事件监听
        $("#" + layero.attr("id"))
          .off("click", ".layui-layer-min")
          .on("click", ".layui-layer-min", function (e) {
            // 阻止事件冒泡
            // ** 这个如果不设置会和下面的事件冲突
            e.stopPropagation();
            // 用添加 layui-hide class的方式来隐藏窗口页面
            // 这个在setTop的操作时有对应的 处理
            layero
              .removeClass(config.constant.HIDE)
              .addClass(config.constant.HIDE);
            // 修改窗口配置属性
            // 最小化标志设置为 true
            config.pageDescribe[id].min = true;
            // 可视化标志设置为false
            config.pageDescribe[id].show = false;
            // 选中配置为false
            config.pageDescribe[id].select = false;
            // 最小化之后取消置顶标志
            if (config.constant.TOP_INDEX == id) config.constant.TOP_INDEX = 0;
            // 这个之前的修改，后面全部隐藏就没用到了，这个也是layui.layer里面可能遇到的问题
            // 只要一次min就会添加上这条属性，一直标志它处于最小化
            // config.pageDescribe[id].parent.removeAttr("minleft");
          });
        // 窗口点击事件监听，点击一下就setTop了
        /**
         * 这个是修改layui.layer里面原生的setTop方法
         * 实现一种点击窗口，那这个窗口就自动切换成最顶层层级的窗口
         * 达到一种窗口切换的效果
         *
         * 因为事件是加在窗口上面的 不含上面的 title操作条
         * 所以窗口里面的点击事件可以加上阻止事件冒泡来防止触发到这个事件
         *
         */
        if (!config.pageDescribe[id].click) {
          config.pageDescribe[id].click = true;
          let that = this;
          layero.on("click", function () {
            // 如果使用多窗口模式才触发
            !config.constant.TOP_LAYER_ONLY && that.setTop(id);
          });
        }
        // 最后setTop一下,防止其它操作的影响，保持新开的窗口处于最顶层级
        this.setTop(id);
      },

      /**
       * 成功关闭页面之后的回调函数
       * @param {*} id 这个指在pageDescribe配置项里面的key或者id，可以通过open方法弹出该窗口
       */
      end(id) {
        // 删除多窗口缓存
        // 标记了topid,说明这个是一个临时的子配置对象，topid的值是它对应的父对象的 key或者id
        if (config.pageDescribe[id].topid) {
          let index = config.pageDescribe[id].topid;
          // 父对象的临时复合参数如果没有创建就自动创建
          if (!config.pageMultiple[index]) config.pageMultiple[index] = {};
          let _k = null;
          // 遍历父对象对应的临时复合参数，如果有和当前的id匹配上的，就将它删除
          _.each(config.pageMultiple[index], function (v, k) {
            if (v == id) {
              _k = k;
              return false;
            }
          });
          if (_k) delete config.pageMultiple[index][_k];
        }
        // 在 windowsMap 配置项里面移除这一项
        if (config.temp.windowsMap[id]) {
          delete config.temp.windowsMap[id];
        }
        // 获取分组ID
        let parentid = config.pageDescribe[id].topid || id;
        /**
         * 过滤  已经打开的窗口/分组 数据
         *
         * 遍历数据  (_.remove  函数会自动移除返回值为true的一项)
         * 首先遍历组
         *      if  与之不匹配  返回false
         *      if  与之匹配   遍历它下面的childern
         *              移除与id匹配的childern
         *              如果过滤后childern的length>0 说明分组下面还有别的窗口，就保留这个分组
         *              如果childern的length=0 说明这个分组被清空了，就返回true 移除这个分组
         *
         */
        let list = _.cloneDeep(config.base.livelyPool);
        _.remove(list, function (v) {
          // 无关的分组直接返回false来保留
          if (v.id != parentid) return false;
          // 移除分组下面该id匹配的项
          _.remove(v.children, (v1) => v1 == id);
          // 如果分组的children长度为0就移除这个分组
          return v.children.length == 0;
        });
        // 更新topIndex
        config.constant.TOP_INDEX = null;
        // 更新动态数据
        config.base.livelyPool = list;
        // 当前的小窗口如果出现了一个窗口也需要移除,也是移除id相同的元素
        let hoverList = _.cloneDeep(config.base.hoverPool);
        _.remove(hoverList, (v) => v.id == id);
        // 更新动态数据
        config.base.hoverPool = hoverList;
        // 移除配置参数中已经添加了点击事件的标记，允许下一次打开的新窗口添加点击事件
        if (config.pageDescribe[id]) config.pageDescribe[id].click = false;
        // 最后再移除,新增的临时配置参数
        if ("child" == config.pageDescribe[id].porperty) {
          delete config.pageDescribe[id];
        } else {
          // 一般的配置参数需要将 parent 和 index移除, 至于VM在定义end的时候就已经移除了。不移除也没得关系
          // parent 窗口打开的时候记录这个窗口的jq对象
          // index  由layui.layer.open方法返回的参数
          config.pageDescribe[id].index = null;
          config.pageDescribe[id].parent = null;
        }
      },

      /**
       * 页面关闭之前的回调函数
       * @param {*} id  这个是配置对象pageDescribe里面的 key或者id
       * @returns
       */
      beforeDestroy(id) {
        let that = this;
        // 获取当前窗口对象
        let ele = config.pageDescribe[id].parent;
        // 处理一下当前页面的东西，这个按下后就要手动关闭页面了
        // 如果不使用 修改关闭动画，直接执行关闭操作，这样的就是layui默认的关闭动画了
        if (!config.constant.LAYER_CLOSE_EFFECTS) return this.doCloseLayer(id);
        // 如果开启修改关闭动画，但是没有生成快照，还是不行的，也是走默认的关闭方式
        if (!config.pageDescribe[id].snapshoot) return this.doCloseLayer(id);
        // 切换快照地址，这一步要求快照地址正确，所以如果是url地址就需要先转换
        document.documentElement.style.setProperty(
          "--snapshoot",
          "url(" + config.pageDescribe[id].snapshoot + ")"
        );
        // 启动关闭特效
        return that.disappear(id);
      },

      /**
       * 启动关闭特效
       * 1.根据当前窗口的位置生成一个近似的div
       * 2.根据参数RADIUS来拆分窗口，生成 RADIUS * RADIUS 个div
       * 3.设置每个div的样式，让快照拼凑出窗口的模样
       * 4.执行动画
       * 5.隐藏并删除div
       * @param {*} id 这个是配置对象pageDescribe里面的 key或者id
       */
      disappear(id) {
        // 获取窗口对象，获得它的基础属性，宽 高 位置 层级
        let ele = config.pageDescribe[id].parent;
        let width = ele.width();
        let height = ele.height();
        let zIndex = ele.css("zIndex");
        // 取成 RADIUS * RADIUS 的整块
        let _width = Math.ceil(width / config.constant.RADIUS);
        let _height = Math.ceil(height / config.constant.RADIUS);
        // 实际的高宽要根据最后拆分的小div来确定
        width = _width * config.constant.RADIUS;
        height = _height * config.constant.RADIUS;
        let left = ele.offset().left;
        let top = ele.offset().top;
        // 创建一个dom完美的覆盖在原来的弹层上面。
        let $dom = $(
          ` <div style = "position:absolute; top:${top}px; left:${left}px; width:${width}px; height:${height}px; z-index: ${zIndex}; margin:0; padding:0; overflow:hidden; background-color:transparent;" ></div> `
        );
        $("body").append($dom);
        let _element = document.createElement("div");
        // 添加这个className是因为统一设置了它的背景图片是刚才那张快照
        _element.className = "disappear-part";
        _element.style.cssText =
          "width:" +
          config.constant.RADIUS +
          "px;height:" +
          config.constant.RADIUS +
          "px; ";
        // 创建文档碎片
        let frag = document.createDocumentFragment();
        for (let h = 0; h < _height; h++) {
          for (let w = 0; w < _width; w++) {
            let _ele = _element.cloneNode(true);
            // 设置样式
            _ele.style.backgroundPosition =
              "-" +
              w * config.constant.RADIUS +
              "px " +
              "-" +
              h * config.constant.RADIUS +
              "px";
            _ele.style.top = h * config.constant.RADIUS + "px";
            _ele.style.left = w * config.constant.RADIUS + "px";
            _ele.setAttribute("pos-h", h);
            _ele.setAttribute("pos-w", w);
            frag.appendChild(_ele);
          }
        }
        // 添加进入页面
        $dom.append(frag);

        /**
         * 创建一个临时函数，计算极大值与极小值中间的一个随机值
         * @param {*} min 极大值
         * @param {*} max 极小值
         * @returns
         */
        function randNum(min, max) {
          return Math.random() * (max - min) + min;
        }

        // 为每一个小div设置动画
        $dom.find(".disappear-part").each(function () {
          // 拿到刚刚在设置时插入的属性，表示它处于横排的那一排竖排的那一排
          let h = this.getAttribute("pos-h");
          let w = this.getAttribute("pos-w");
          /**
           * 计算，如果它处于上部  h - _height / 2 < 0
           *    那么它的落点 top应该是 向上移一半个整体高度
           *    反之应该向下移一半个整体高度
           */
          let _top =
            h - _height / 2 > 0
              ? h * config.constant.RADIUS +
              (h - _height / 2) * config.constant.RADIUS
              : h * config.constant.RADIUS -
              (_height / 2 - h) * config.constant.RADIUS;
          /**
           * 计算，如果它处于左部  w - _width / 2 < 0
           *    那么它的落点 left应该是 向左移一半个整体宽度
           *    反之应该向右移一半个整体高度
           */
          let _left =
            w - _width / 2 > 0
              ? w * config.constant.RADIUS +
              (w - _width / 2) * config.constant.RADIUS
              : w * config.constant.RADIUS -
              (_width / 2 - w) * config.constant.RADIUS;

          // 计算出他们距离对面的高度差
          let sh = h - _height / 2 > 0 ? _height - h : h;
          // 计算出他们距离对面的宽度差
          let sw = w - _width / 2 > 0 ? _width - w : w;
          // 计算出他们距离中心的高度差
          let st = h - _height / 2 > 0 ? h - _height / 2 : _height / 2 - h;
          // 计算出他们距离中心的宽度差
          let sl = w - _width / 2 > 0 ? w - _width / 2 : _width / 2 - w;
          // 计算出最后缩小的比例，越往里面越小
          let scale = ((st + sl) / (_height / 2 + _width / 2)) * 0.8;
          /**
           * 开启动画
           * top 取自己上/下移半个高度单位的附近 config.constant.RADIUS 邻域。
           * left 取自己左/右移半个宽度单位的附近 config.constant.RADIUS 邻域。
           * width 越靠近中心的越小
           * height 越靠近中心的越小
           * scale 越靠近中心的越小
           * 变成圆角
           * 逐渐透明
           */
          $(this).animate(
            {
              top:
                randNum(
                  _top - config.constant.RADIUS,
                  _top + config.constant.RADIUS
                ) + "px",
              left:
                randNum(
                  _left - config.constant.RADIUS,
                  _left + config.constant.RADIUS
                ) + "px",
              width: (sw / _width) * config.constant.RADIUS + "px",
              height: (sh / _height) * config.constant.RADIUS + "px",
              transform: "scale(" + scale + ")",
              "border-radius": config.constant.RADIUS + "px",
              opacity: 0,
            },
            3000,
            "swing"
          );
        });
        // 执行普通 的关闭方法
        // 由于层级较高，普通的关闭方法动画会直接被覆盖
        this.doCloseLayer(id);
        // 动画2s后移除dom
        setTimeout(function () {
          $dom.remove();
        }, 2000);
      },

      /**
       * 关闭窗口的方法，
       * 内部有几个方法在调用，作为方法的收尾
       * 外面建议调用 下面的 close 方法来关闭窗口
       * @param {*} id  这个是配置对象pageDescribe里面的 key或者id
       */
      doCloseLayer(id) {
        // 因为前置方法中可能已经有关闭窗口的操作了，这里只是查漏补缺
        // 获取index,调用layui原生方法关闭窗口即可
        let _index = config.pageDescribe[id].index;
        _index && layui.layer.close(_index);
        // 清除index，虽然其它地方做过了，这里仅仅查漏
        config.pageDescribe[id].index = null;
      },

      /**
       * 保留方法，内部有调用，不推荐使用
       * @param {*} id
       */
      _restore(id) {
        let _index = config.pageDescribe[id].index;
        if (_index) {
          try {
            layui.layer.restore(_index);
          } catch (error) { }
        }
      },

      /**
       * 最小化恢复，
       * @param {*} id 这个是配置对象pageDescribe里面的 key或者id
       */
      restore(id) {
        let _index = config.pageDescribe[id].index;
        if (_index) {
          // 调用原生方法恢复窗口
          try {
            layui.layer.restore(_index);
          } catch (error) { }
          // 设置 pageDescribe 的最小化参数
          config.pageDescribe[id].min = false;
          config.pageDescribe[id].select = true;
          config.pageDescribe[id].show = true;
          // 给窗口的jq对象添加 layui-hide 的 className
          config.pageDescribe[id].parent.removeClass(config.constant.HIDE);
          //.removeAttr("minleft");
        }
      },

      /**
       * 最小化窗口，暂无使用
       * 保留方法，内部有调用，不推荐使用
       * @param {*} index 这个是配置对象pageDescribe里面的 key或者id
       */
      min(id) {
        // 获取index证明这个窗口被创建出来了
        let _index = config.pageDescribe[id].index;
        if (_index) {
          // 调用原生方法最小化窗口
          layui.layer.min(_index);
          // 设置 pageDescribe 的最小化参数
          config.pageDescribe[id].min = true;
          config.pageDescribe[id].select = false;
          config.pageDescribe[id].show = false;
          // 给窗口的jq对象添加 layui-hide 的 className
          config.pageDescribe[id].parent
            .removeClass(config.constant.HIDE)
            .addClass(config.constant.HIDE);
          //.removeAttr("minleft");
        }
      },

      /**
       * 窗口关闭方法，
       * 调用beforeDestroy关闭窗口
       * @param {*} id 这个是配置对象pageDescribe里面的 key或者id
       */
      close(id) {
        this.beforeDestroy(id);
      },

      /**
       * 最小化当前全部窗口
       */
      minAll() {
        let _this = this;
        _.each(config.temp.windowsMap, (v, k) => _this.min(k));
      },

      /**
       * 关闭当前全部窗口
       */
      closeAll() {
        let _this = this;
        _.each(config.temp.windowsMap, (v, k) => _this.close(k));
      },

      /**
       * 展示小窗口
       * 在鼠标移除下面的选项卡分组的时候，弹出分组下面的所有窗口代表的小窗口预览div
       */
      showWindowTip(e) {
        let $this = $(e.target);
        let that = this;
        // 添加选中样式
        this.parent.find(".li-select-tip").removeClass("li-select-tip");
        $this.addClass("li-select-tip");
        // 获取分组ID
        let id = $this.attr("lay-id");
        let $target = this.parent.find(
          "." + config.constant.CONTAINER_TIPS_CLASSNAME
        );
        /**
         * select-tip-flag 添加显示在靠近底部的样式
         * no-select-tip   标记这个dom暂时不受其它事件的影响
         */
        $target
          .removeClass("no-select-tip")
          .removeClass("select-tip-flag")
          .addClass("select-tip-flag")
          .addClass("no-select-tip");
        // 遍历整个动态分组，查找到对应的分组
        _.each(that.livelyPool, function (v) {
          if (v.id == id) {
            // 清空参数
            let templist = [];
            that.$data.hoverPool = [];

            // 将分组中的children对应的配置参数写入到临时列表里面
            _.each(v.children, function (v1) {
              templist.push({
                id: config.temp.windowsMap[v1].id,
                name: config.temp.windowsMap[v1].name,
              });
            });

            // 参数赋值
            that.$data.hoverPool = templist;

            // 从pageDescribe里面获取快照
            let _config = config.pageDescribe[id];
            // 有快照就切换快照地址
            if (_config.snapshoot) {
              // 切换快照地址
              document.documentElement.style.setProperty(
                "--snapshoot",
                "url(" + _config.snapshoot + ")"
              );
            } else {
              // 没有快照切换成空，以免上一次的影响
              document.documentElement.style.setProperty("--snapshoot", "none");
            }

            // 获取当前dom元素的位置
            let left = $this.get(0).getBoundingClientRect().left;
            let width = $this.outerWidth();
            let finalLeft = left - ($target.width() - width) / 2;

            // 设置tips位置
            $target.css({ left: finalLeft < 0 ? 0 : finalLeft + "px" });

            // 添加select-tip样式
            $target.removeClass("select-tip").addClass("select-tip");

            // 移除no-select-tip标记
            setTimeout(() => $target.removeClass("no-select-tip"), 300);
          }
        });
      },

      /**
       * 鼠标移上小窗户保持检查的状态，
       * 一直给下面的分组选项卡更新 一直添加select-tip-flag样式
       */
      hoverTip() {
        let $target = this.parent.find(
          "." + config.constant.CONTAINER_TIPS_CLASSNAME
        );
        if ($target.hasClass("no-select-tip")) return;
        if (!$target.hasClass("select-tip-flag"))
          $target.addClass("select-tip-flag");
      },

      /**
       * 在鼠标移开分组选项卡或者小窗口时，隐藏小窗户
       *
       * 两个class一起作用，防止抖动。
       *    select-tip-flag
       *    select-tip
       *
       * 1.当鼠标离开时会将分组选项卡里面的 select-tip-flag 样式移除，但是如果它跳到了 小窗户 上面，会在上面hoverTip方法里面
       *  将样式重新加回来。
       * 2.如果鼠标短暂的离开分组选项卡和小窗口  select-tip-flag 样式移除 因为受其它的影响，暂时还不会关闭小窗口
       * 3.如果超过一定时间没有进入其它方法将 select-tip-flag 样式补回来，就认为是真的离开了，执行关闭小窗口的方法
       *
       */
      hideWindowTip() {
        let _this = this;
        let $target = this.parent.find(
          "." + config.constant.CONTAINER_TIPS_CLASSNAME
        );
        // select-tip-flag 样式移除
        $target.removeClass("select-tip-flag");
        setTimeout(function () {
          // 如果超时之后样式还是没有补回来
          if (!$target.hasClass("select-tip-flag")) {
            /**
             * 移除select-tip样式
             * select-tip   和 select-tip-flag 样式有一个小窗口就不会关闭，
             * 现在两个都移除了，它就关闭了
             */
            $target.removeClass("select-tip");
            // 分组选项卡  取消选中样式
            _this.parent.find(".li-select-tip").removeClass("li-select-tip");
            // 添加一个class这个class在hover时阻止触发事件，阻止关闭动画时触发的影响
            $target.removeClass("no-select-tip").addClass("no-select-tip");
            // 移除标记
            setTimeout(() => $target.removeClass("no-select-tip"), 300);
          }
        }, 300);
      },

      /**
       * 通过小窗户关闭页面
       * 通过点击上面的红叉按钮关闭页面
       *
       */
      closeFromTip(e) {
        e.stopPropagation();
        e.preventDefault();
        let id = $(e.target).parent().attr("wid");
        // 关闭窗口
        this.close(id);
        // 清除预览参数,先给这个预览主窗口加个判断，防止再触发move方法
        let $target = this.parent.find(
          "." + config.constant.CONTAINER_TIPS_CLASSNAME
        );
        if (!$target.hasClass("no-select-tip")) $target.addClass("no-select-tip");
        this.hover = {};
        setTimeout(function(){
          $target.removeClass("no-select-tip");
        },300);
        // 下面的关闭功能暂时去掉
        // // 这里的小窗口将会移除，但是需要点击后隐藏的效果
        // let $parent = $(e.target).parent().parent();
        // /**
        //  * 模拟隐藏小窗口的效果
        //  * 去掉 select-tip  和  select-tip-flag
        //  * 标记上  no-select-tip
        //  * ---
        //  * 移除标记
        //  */
        // $parent
        //   .removeClass("select-tip")
        //   .removeClass("select-tip-flag")
        //   .addClass("no-select-tip");
        // setTimeout(() => $parent.removeClass("no-select-tip"), 300);
      },

      /**
       * 点击小窗口将当前窗口置顶展示
       * @param {*} id 这个是配置对象pageDescribe里面的 key或者id
       */
      setHoverTop(id) {
        // 调用方法置顶
        this.setTop(id);
        // 清除预览参数
        this.hover = {};
        // 隐藏弹窗
        let $parent = this.parent.find(
          "." + config.constant.CONTAINER_TIPS_CLASSNAME
        );
        /**
         * 模拟隐藏小窗口的效果
         * 去掉 select-tip  和  select-tip-flag
         * 标记上  no-select-tip
         * ---
         * 移除标记
         */
        $parent
          .removeClass("select-tip")
          .removeClass("select-tip-flag")
          .addClass("no-select-tip");
        setTimeout(() => $parent.removeClass("no-select-tip"), 300);
      },

      /**
       * 点击小窗口下面的li标签， 显示当前窗口
       * 当且仅当这个下面只有一个子窗口时有效
       */
      setGroupTop(id) {
        let _this = this;
        _.each(config.base.livelyPool, function (v) {
          if (v.id == id) {
            if (v.children.length == 1) {
              _this.setTop(v.children[0]);
              // 清除预览参数
              _this.hover = {};
              // 隐藏弹窗
              let $parent = _this.parent.find(
                "." + config.constant.CONTAINER_TIPS_CLASSNAME
              );
              /**
               * 模拟隐藏小窗口的效果
               * 去掉 select-tip  和  select-tip-flag
               * 标记上  no-select-tip
               * ---
               * 移除标记
               */
              $parent
                .removeClass("select-tip")
                .removeClass("select-tip-flag")
                .addClass("no-select-tip");
              setTimeout(() => $parent.removeClass("no-select-tip"), 300);
            }
          }
        });
      },

      /**
       * 预览鼠标所在的小窗口所对应的大窗口
       * @param {*} id  这个是配置对象pageDescribe里面的 key或者id
       */
      appendHover(id) {
        // 0 的特殊处理，代表关闭全部
        if (0 == id) {
          this.hover = { id: 0 };
          return;
        }
        let $target = this.parent.find(
          "." + config.constant.CONTAINER_TIPS_CLASSNAME
        );
        if ($target.hasClass("no-select-tip")) return;
        // 获取窗口配置对象
        let _config = config.pageDescribe[id];
        // 将当前窗口的层级上升为顶层加1
        this.tabsBody(id).css("z-index", config.constant.MAX_INDEX + 1);
        // 如果窗口被最小化，就恢复
        if (!!_config.min) this._restore(id);
        // 如果窗口被隐藏，就恢复
        if (!_config.show) this.tabsBody(id).removeClass(config.constant.HIDE);
        // 设置此id为正在被预览的id
        // this.hover.id = id;
        this.hover = { id: id };
      },

      /**
       * 鼠标在小窗口上面移动的时候需要动态的判断是否切换了预览目标
       * @param {*} id 这个是配置对象pageDescribe里面的 key或者id
       */
      moveHover(id) {
        // 0 的特殊处理，代表关闭全部
        if (0 == id) {
          this.hover = { id: 0 };
          return;
        }
        let $target = this.parent.find(
          "." + config.constant.CONTAINER_TIPS_CLASSNAME
        );
        if ($target.hasClass("no-select-tip")) return;
        // 如果选中的id并不是正在被预览的id
        if (this.hover.id != id) {
          // 结束正在被预览的id
          this.hover.id && this.removeHover(this.hover.id);
          // 预览选中的id
          this.appendHover(id);
        }
      },

      /**
       * 鼠标离开小窗口时结束正在被预览的窗口
       * @param {*} id 这个是配置对象pageDescribe里面的 key或者id
       */
      removeHover(id) {
        // 0 的特殊处理，代表关闭全部
        if (0 == id) {
          this.hover = {};
          return;
        }
        let $target = this.parent.find(
          "." + config.constant.CONTAINER_TIPS_CLASSNAME
        );
        // 这里不需要限制，离开的时候是很突然的
        // if ($target.hasClass("no-select-tip")) return;
        if (this.hover.id == id) {
          // 获取窗口配置对象
          let _config = config.pageDescribe[id];
          // let option = this.hover;
          // 将层级恢复到之前的层级
          this.tabsBody(id).css("z-index", _config.nowIndex);
          // 如果之前被标记为最小化就恢复最小化
          if (!!_config.min) this.min(id);
          // 如果之前是隐藏状态就添加 上 layui-hide
          if (!_config.show)
            this.tabsBody(id)
              .removeClass(config.constant.HIDE)
              .addClass(config.constant.HIDE);
          // 移除正在预览的参数配置
          this.hover = {};
        }
      },

      showMenu(e) { },
    },
  });

  /**
   * 关系集合
   *
   * leaderKey --Array
   *            [ follower  VM , params, strategy,  ]
   *
   */
  let register = {};

  /**
   * 为了防止多实例时产生多个leader的问题，缓存了一份 leader与 epochId之间的关系
   */
  // let leaderRegister = {};

  /**
   * leader.lead方法
   * 声明这个视图对象作为leader出现，允许其它follower注册
   * // 传入 leader 的id 后面的follower注册可以区分，存放的关系也是一个map  { id : [arr]}
   * //   follower传入的是一个对象，它还要表明它的同步条件
   * // 传入 key    默认是里面的form属性  这个属性共享
   *
   * // leader的属性发生改变的时候有条件的去修改follower里面的值。
   * // follower不存在时移除 _.remove  返回false
   * @param {*} epochId  leaderKey
   * @param {*} router   leader指定暴露的路由 ，默认是form
   * // 注意，程序设计问题，暴露的路由后面不能扩展，初始化就需要完善
   *
   */
  layui.binder.Method("lead", function (epochId, router = "form") {
    // 为了防止多实例时产生多个leader的问题，缓存了一份 leader与 epochId之间的关系
    // if (!leaderRegister[epochId]) {
    //   leaderRegister[epochId] = this;
    // } else {
    //   console.warn("leader已产生，不能重复绑定");
    //   return;
    // }
    // 创建 leader 与 follower 关系集合，这样的设计是在 leader初始化之前 follower也可以自行注册
    if (!register[epochId]) register[epochId] = [];
    // 为自己创建一个监视  router 下面的参数变化
    this.$watch(router, {
      immediate: true,
      deep: true,
      handler: function (value) {
        if (value == undefined) return;
        /**
         * 获取当前 router 对应的值
         * 回调函数放在后置事件，此时值已经修改完毕
         */
        let routerValue = this.proxy.getValue.call(this, router);
        // 移除已过时的 follower
        _.remove(register[epochId], (v) => !v.VM.isAlive);
        // follower 为空就直接返回
        if (register[epochId].length == 0) return;
        // 根据拒绝策略，筛选出符合条件的 follower
        _.each(
          _.filter(register[epochId], (v1) => {
            // 返回无拒绝策略，或者拒绝策略函数执行成功返回false的
            return (
              !v1.strategy ||
              (_.isFunction(v1.strategy) &&
                !v1.strategy.call(v1.VM, routerValue))
            );
          }),
          function (follower) {
            // 更新 follower 选择跟随的属性
            if (!follower.params) {
              // 不传入默认就是全部替换,router下面的所有key
              _.each(Object.keys(routerValue), function (key) {
                if (follower.VM.$data[router][key] !== undefined)
                  follower.VM.$data[router][key] = routerValue[key];
              });
            } else {
              _.each(String(follower.params).split(","), function (key) {
                if (
                  follower.VM.$data[router][key] !== undefined &&
                  routerValue[key] !== undefined
                )
                  follower.VM.$data[router][key] = routerValue[key];
              });
            }
          }
        );
      },
    });
  });

  /**
   * follower.followLead方法
   * 指定需要follow的lead的epochId
   * 指定它的同步条件，不传默认是无条件同步
   * @param {*} epochId  leaderKey
   * @param {*} params   指定暴露的同步的参数集合，以，分隔。这个参数是 router 下面的key
   * @param {*} strategy 拒绝策略，返回true拒绝这一次的更新，返回false接收这一次的更新
   *  strategy  Function (routerValue)  函数执行的上下文就是当前视图对象
   * 如果第二个参数是一个function，默认 params 为空
   *
   */
  layui.binder.Method("followLead", function (epochId, params, strategy) {
    // 初始化参数
    let _params = params !== undefined && _.isString(params) ? params : "";
    let _strategy =
      params !== undefined && _.isFunction(params)
        ? params
        : strategy !== undefined && _.isFunction(strategy)
          ? strategy
          : null;
    if (!register[epochId]) register[epochId] = [];
    // 放入缓存中，直到对象销毁后才能被 leader 移除
    // 这里刚刚加入是不能触发同步状态的，因为这个时候leader有没有被创建出来都说不一定
    register[epochId].push({
      VM: this,
      params: _params,
      strategy: _strategy,
    });
  });

  /**
   * linkWindow,仅建立关联关系，不提供，也不能直接提供打开窗口的方法
   *
   *
   */
  layui.binder.Method("linkWindow", function (VM) {
    let that = this;
    // 当前视图没有初始化就进行初始化
    if (!that.linkConfig) initLink.call(this);
    /**
     * 检查，当前的 linkConfig里面 VM
     * 1.如果为空   可能是第一次初始化
     * 2.如果不为空 可能是抛弃上一次的link，创建新的连接，这种断开方式就是提升自己的epochId
     */
    if (!that.linkConfig.VM) that.epochId++;
    // 首先确定视图对象
    if (VM instanceof layui.binder) {
      // 传入的视图没有初始化就进行初始化
      if (!VM.linkConfig) initLink.call(VM);
      // 双方交换视图对象
      that.linkConfig.VM = VM;
      VM.linkConfig.VM = that;
      // 如果现在的 epochId 和传入视图的 epochId 相同，那么说明他们同时提升epochId 才能断开之前的联系
      if (that.epochId == VM.epochId) that.epochId++;
      // 同步对方的 epochId
      // 这里先同步 epochId 是防止同步属性时会影响到之前link的窗口属性
      VM.epochId = that.epochId;
      // 同步两个视图的form属性
      let formData = that.proxy.getValue("form");
      _.each(Object.keys(formData), (v) => (VM.$data.form[v] = formData[v]));
    }
  });

  /**
   * 初始化 linkWindow
   * 1.创建基础参数 linkConfig 和 epochId
   * 2.创建事件监听
   *
   * linkConfig里面有两个属性 VM 和 epochId
   * 如果 VM 不存在 或者 过期 isAlive = false 说明需要link的视图已不可用
   * 如果 epochId 和自己的 epochId 不相等 说明关联的视图对象已经被其它关联了
   *
   */
  function initLink() {
    let that = this;
    // 第一次进行初始化
    that.linkConfig = {};
    that.epochId = 0;
    // 监听关联的信息 form
    // 这一步是 从当前视图的改变去改变另一个视图的过程
    that.$watch("form", {
      immediate: true,
      deep: true,
      handler() {
        // 判断 VM 存在 并且 VM 有效
        if (!that.linkConfig.VM || !that.linkConfig.VM.isAlive) return;
        if (
          that.linkConfig.VM instanceof layui.binder &&
          that.linkConfig.VM.epochId == that.epochId
        ) {
          let formData = that.proxy.getValue("form");
          _.each(
            Object.keys(formData),
            (v) => (that.linkConfig.VM.$data.form[v] = formData[v])
          );
        } else {
          // 如果它的 epochId 对不上，可以认为单方面断开了
          that.linkConfig = {};
        }
      },
    });
  }

  exports("windows", {});
});
